package com.mumu.muPictureBackend.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mumu.muPictureBackend.config.UnsplashConfig;
import com.mumu.muPictureBackend.mapper.SpaceMapper;
import com.mumu.muPictureBackend.model.entity.Space;
import com.mumu.muPictureBackend.model.enums.SpaceEnum;
import com.mumu.muPictureBackend.service.SpaceService;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.json.JSONArray;
import org.json.JSONObject;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.mumu.muPictureBackend.constant.CaCheConstant;
import com.mumu.muPictureBackend.constant.UserConstant;
import com.mumu.muPictureBackend.exception.BusinessException;
import com.mumu.muPictureBackend.exception.ErrorCode;
import com.mumu.muPictureBackend.exception.ThrowUtils;
import com.mumu.muPictureBackend.manager.CosManager;
import com.mumu.muPictureBackend.manager.upload.FilePictureUpload;
import com.mumu.muPictureBackend.manager.upload.PictureUploadTemplate;
import com.mumu.muPictureBackend.manager.upload.URLPictureUpload;
import com.mumu.muPictureBackend.mapper.PictureMapper;
import com.mumu.muPictureBackend.model.dto.pictrue.*;
import com.mumu.muPictureBackend.model.entity.Picture;
import com.mumu.muPictureBackend.model.entity.User;
import com.mumu.muPictureBackend.model.enums.PictureRoleEnum;
import com.mumu.muPictureBackend.model.enums.UserRoleEnum;
import com.mumu.muPictureBackend.model.vo.PictureVO;
import com.mumu.muPictureBackend.model.vo.UploadPictureResult;
import com.mumu.muPictureBackend.model.vo.UserVO;
import com.mumu.muPictureBackend.service.PictureService;
import com.mumu.muPictureBackend.service.UserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture> implements PictureService {
    @Resource
    private CosManager cosManager;
    @Resource
    private FilePictureUpload filePictureUpload;
    @Resource
    private URLPictureUpload urlPictureUpload;
    @Autowired
    private PictureMapper pictureMapper;


    /**
     * 校验图片是否合格 给增删改查之后校验用的
     *
     * @param picture
     */
    public void validPicture(Picture picture) {
        ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR, "图片为空");
        Long id = picture.getId();
        String url = picture.getUrl();
        String introduction = picture.getIntroduction();
        ThrowUtils.throwIf(id == null, ErrorCode.OPERATION_ERROR, "id不能为空");
        if (StrUtil.isNotBlank(url))
            ThrowUtils.throwIf(url.length() > 1024, ErrorCode.OPERATION_ERROR, "url过长");
        if (StrUtil.isNotBlank(introduction))
            ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.OPERATION_ERROR, "简介过长");
    }

    /**
     * 校验当前登录用户是否有权操作该图片
     *
     * @param picture
     * @param loginUser
     */
    public void checkPictureAuth(Picture picture, User loginUser) {
        Long spaceId = picture.getSpaceId();
        //如果是公共图库的图片 那么只有上传该图片的用户或者是管理员才能操作
        if (spaceId == null) {
            if (!picture.getUserId().equals(loginUser.getId()) && !isAdmin(loginUser)) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR);
            }
        } else {
            //否则 是个人空间图库的图片 那么只有是当前登录用户自己的图片才能操作
            // 获取当前空间
//            Space space = spaceService.getById(picture);
//            if (space == null || !space.getUserId().equals(loginUser.getId())) {
//                throw new BusinessException(ErrorCode.OPERATION_ERROR);
//            }
            //优化:上传图片的时候 图片记录已经存储了userId 和 该图片所在的空间的userId也是关联的呀
            if (!picture.getUserId().equals(loginUser.getId())) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR);
            }
        }
    }

    @Resource
    SpaceService spaceService;
    @Resource
    SpaceMapper spaceMapper;

    /**
     * 1. 更新操作时:先从cos中删除图片 再更新数据库中的网址+事务回滚 缺点:如果cos删除成功 但是数据库更新失败->那么就会遗留老数据 并且原有的照片也已经删除了
     * 2. 更新操作时:先从数据库中更新网址 再从cos中删除图片+事务回滚 优点:如果数据库更新失败 数据库会回滚 跳过cos删除 如果cos删除失败(没有删除掉) 那么事务回滚数据库 老照片保留
     *
     * @param inputSource          要上传的图片可以是url可以是本地file文件
     * @param uploadPictureRequest 图片的id 如果id不存在->保存操作 id存在->更新操作
     * @param loginUser            登录的用户 如果为null->未登录->拦截     存在->已登录->放行存储
     * @return
     */

    @Transactional
    @Override
    public PictureVO uploadPicture(Object inputSource, UploadPictureRequest uploadPictureRequest, User loginUser) {
        ThrowUtils.throwIf(uploadPictureRequest == null, ErrorCode.NOT_FOUND_ERROR);
//        1. Service思路:
//        2. 先判断UserId是否为null 为null表示未登录
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR, "先登录再来存储你的图片吧~");

        //如果该图片是长传到空间
        Space space = null;
        Long spaceId = uploadPictureRequest.getSpaceId();
        if (spaceId != null) {
            //那么该空间不能为空 并且当前登录用户是该空间的主人
            //1. 获取原有空间 并且校验
            space = spaceService.getById(spaceId);
            ThrowUtils.throwIf(space == null, ErrorCode.OPERATION_ERROR);
            if (!space.getUserId().equals(loginUser.getId())) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "不要添加图片到别人空间");
            }
            //判断是否超额(后置判断)
            if (space.getTotalCount() > space.getMaxCount() || space.getTotalSize() > space.getMaxSize()) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "余额不足");
            }
        }
//        3. 获取图片id
        Long picId;
        Picture oldPicture = null;
        //不为空就getId赋值
//     如果是更新 那么id有值 如果是保存 那么id无值
        picId = uploadPictureRequest.getId();
//        如果该图片id存在 那么就表示是更新 那么先根据该id查询一下 该图片信息是否存在 如果存在 那么就放行 不存在->抛错
        if (picId != null) {
            oldPicture = getById(picId);
            //当是修改操作时又根据id找不到对应的图片记录
            ThrowUtils.throwIf(oldPicture == null, ErrorCode.OPERATION_ERROR, "更新图片失败 在数据库中找不到该图片信息");
//       加一层校验 该图片的上传者是不是当前的登陆用户者 并且不是管理员
            ThrowUtils.throwIf(!Objects.equals(oldPicture.getUserId(), loginUser.getId()) && !isAdmin(loginUser), ErrorCode.OPERATION_ERROR, "!!!违规操作!!! 篡改他人图片");

            if (spaceId == null) {
                //用老图片的spaceId给新图片
                spaceId = oldPicture.getSpaceId();
            } else {
                //如果老图片和新图片的spaceId不一样就抛出异常
                throw new BusinessException(ErrorCode.OPERATION_ERROR);
            }
        }

//        4. 先将图片保存到cos中 接下来就保存到数据库中
//        路径的前缀就用userId来标识,分类  public是父文件夹 图片的文件夹分类就是public/用户id
        String pathPrefix;
        //如果上传的是公共图库
        if (spaceId == null) {
            pathPrefix = String.format("public/%s", loginUser.getId());
        } else {
            //如果上传的是个人空间 那么标识就是空间id了
            pathPrefix = String.format("space/%s", spaceId);
        }
//      5.   将图片上传到cos 返回值是图片的基本信息

        // 5.1 定义UploadPictureResult 用于接收返回的图片信息
        UploadPictureResult pictureResultInfo = null;
        // 5.2 如果是本地文件类型 就将pictureUploadTemplate(父)赋值成filePictureUpload(子) filePictureUpload是用于上传本地文件的
        PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
        if (inputSource instanceof String) {
            // 5.3 如果上传的类型是String那么就表示是url类型的 那么就用urlPictureUpload来上传url
            pictureUploadTemplate = urlPictureUpload;
        }
        // 5.4 最后将文件上传
        pictureResultInfo = pictureUploadTemplate.uploadRequest(inputSource, pathPrefix);


        log.info("图片上传成功!");
//        6. 如果不为空 那么就表示这次是更新 那么update
//        获取图片信息
        Picture picture = new Picture();
        picture.setUserId(loginUser.getId());//保存图片的用户id存储一下
//        图片属性copy 类型转换->entity
        BeanUtil.copyProperties(pictureResultInfo, picture);
        //更新图片: 如果该用户不是管理员 那么就将该图片的信息更改为待审核 管理员的话自动过审(需要填充审核参数)
        fillReviewParams(picture, loginUser);
//        操作数据库
        picture.setEditTime(LocalDateTime.now());
        //先获取cos中原本的名称
        String fileName = pictureResultInfo.getName();
        //如果请求参数中用户自定义了名称到fileName里面 那么就使用用户自定义的名称
        if (uploadPictureRequest.getFileName() != null && StrUtil.isNotBlank(uploadPictureRequest.getFileName())) {
            fileName = uploadPictureRequest.getFileName();
        }
        picture.setName(fileName);

        //图片上传到空间
        if (spaceId != null) {
            // 容量校验
            long picSize = pictureResultInfo.getPicSize();
            //校验容量
            if (space.getTotalCount() + 1 > space.getMaxCount() || space.getTotalSize() + picSize > space.getMaxSize()) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "容量不够了哦");
            }
            //2. 将当前图片的大小累加到空间已用总大小里面 记录数+1
            spaceMapper.updateSpaceSizeAdd(space.getId(), picSize);
            //这里就不要先更新space对象再调用update了 会出现并发问题 如果来一个图片就直接从数据库中去更新 就不会出现并发问题(有原子性)
//       @MapperScan
//public interface SpaceMapper extends BaseMapper<Space> {
//    // 添加图片后 用于更新空间已用总容量
//    @Update("UPDATE space set tatalSize=totalSize+#{picSize},totalCount=totalCount+1 where id=#{spaceId} and totalSize+#{picSize}<maxSize and totalCount+1<maxCount")
//    void updateSpaceSize(@Param("spaceId") long spaceId, @Param("picSize") long picSize);
//}
        }

//        更新图片
        if (picId != null) {
//            set一波id 然后根据idUpdate该图片
            picture.setId(picId);
            picture.setEditTime(LocalDateTime.now());
//            这里没必要用上面的oldPicture 直接用这个picture就可以了 里面装的是需要更新的
            ThrowUtils.throwIf(!updateById(picture), ErrorCode.SYSTEM_ERROR, "数据库更新失败");
            //从cos中将老图片给删除
            cosDeletePicture(oldPicture);
            return PictureVO.objToVo(picture);
        }
        picture.setCreateTime(LocalDateTime.now());
//        5. 如果图片id 为空 那么就表示这次是上传:save
        //saveOrUpdate:如果该图片有id 那么自动执行update操作 否则执行的是save操作
        ThrowUtils.throwIf(!save(picture), ErrorCode.SYSTEM_ERROR, "数据库保存失败");
        return PictureVO.objToVo(picture);
    }

    private void fillReviewParams(Picture picture, User loginUser) {
        //如果是管理员 那么就自动过审但是需要填充过审日志
        if (isAdmin(loginUser)) {
            picture.setReviewerId(loginUser.getId());
            picture.setReviewMessage("管理员更新图片 自动过审");
            picture.setReviewStatus(PictureRoleEnum.PASS.getValue());
            picture.setReviewTime(LocalDateTime.now());
        } else {
            //不是管理员更新图片 那么就将该图片更改为未审查状态
            picture.setReviewStatus(PictureRoleEnum.REVIEWING.getValue());
        }
    }

//    /**
//     * TODO 不可用 可能设计的就是: 上传图片后会返回该图片的URL 点击这个URL本身就可以下载该图片
//     *
//     * @param pictureDownloadRequest 获取目标图片的id
//     * @param loginUser              获取图片的用户
//     * @param httpServletResponse    response响应体 将图片数据set到这个响应体中 并且标识这是一个直接下载的
//     */
//    @Override
//    public void downloadPicture(PictureDownloadRequest pictureDownloadRequest, User loginUser, HttpServletResponse httpServletResponse) {
//        //1. 校验请求数据
//        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR, "不登陆就不给你下载");
//        ThrowUtils.throwIf(pictureDownloadRequest == null, ErrorCode.NOT_FOUND_ERROR);
//        ThrowUtils.throwIf(pictureDownloadRequest.getId() == null, ErrorCode.NOT_FOUND_ERROR);
//
//        //2. 通过id从数据库中获取该cos地址 然后从cos上获取图片
//        Long id = pictureDownloadRequest.getId();
//        Picture pictureInfo = getById(id);
//        ThrowUtils.throwIf(pictureInfo == null, ErrorCode.NOT_FOUND_ERROR, "数据库中无这个图片");
//        //从cos中获取该图片 并且转换成byte
//        try {
//            String url = pictureInfo.getUrl();
//            byte[] bytes = fileManager.downloadPicture(url);
//            httpServletResponse.setContentType("application/octet-stream;charset=UTF-8");
//            httpServletResponse.setHeader("Content-Disposition", "attachment; filename=" + url);
//        } catch (IOException ioException) {
//            throw new RuntimeException(ioException);
//        }
//
//        return;
//    }


    /**
     *
     * @param uploadPictureRequest 要删除的图片的id
     * @param loginUser            当前登录的用户
     * @return
     */
    @Transactional
    @Override
    public Boolean deletePicture(UploadPictureRequest uploadPictureRequest, User loginUser) {
        //判断是否登录 校验是否有参数
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
        ThrowUtils.throwIf(uploadPictureRequest == null || uploadPictureRequest.getId() == null, ErrorCode.NOT_FOUND_ERROR);
        //已经登录 并且是管理员权限 那么就从数据库中将图片给删除掉
        Long picId = uploadPictureRequest.getId();
        //先将该图片查询出来 需要里面的url 利用该url在cos中删除该图片
        Picture picture = getById(picId);
        ThrowUtils.throwIf(picture == null, ErrorCode.OPERATION_ERROR, "数据库中未查找到该记录");
        //检查是否有权删除图片
        checkPictureAuth(picture, loginUser);
        //如果当前的图片不是自己上传的并且该用户不是管理员 那么就抛出异常即可
        ThrowUtils.throwIf(!Objects.equals(picture.getUserId(), loginUser.getId()) && !isAdmin(loginUser), ErrorCode.FORBIDDEN_ERROR, "为什么要删除别人的图片?");

        //校验成功 那么就删除 这里删除也是逻辑删除 不是真正删除 只是将isDelete更改为1
        ThrowUtils.throwIf(!removeById(picId), ErrorCode.OPERATION_ERROR);

        //空间操作 如果删除的是个人空间的图片 那么需要更新space表
        if (picture.getSpaceId() != null) {
            Long picSize = picture.getPicSize();
            spaceMapper.updateSpaceSizeSub(picture.getSpaceId(), picSize);
        }
        //数据库中删除成功后 再从cos中删除
        cosDeletePicture(picture);

        return true;
    }


    /**
     * 更新图片信息
     *
     * @param updatePictureRequest
     * @return
     */
    @Override
    public Boolean updatePicture(UpdatePictureRequest updatePictureRequest, User loginUser) {
        //1. 校验
        ThrowUtils.throwIf(updatePictureRequest == null || updatePictureRequest.getId() == null, ErrorCode.NOT_FOUND_ERROR);
        //由于前端传过来的数据可以伪造 比如将updatePictureRequest中的userId字段伪造成别人的 所以为了安全一点 我直接通过这个被更新的图片的id来从数据库中查找对应的userid 然后再和当前登录的userId进行比对
        Picture pic = getById(updatePictureRequest.getId());
        ThrowUtils.throwIf(pic == null || !Objects.equals(pic.getUserId(), loginUser.getId()), ErrorCode.FORBIDDEN_ERROR);
        //校验当前用户是否有权更新该图片
        checkPictureAuth(pic, loginUser);
        Picture picture = new Picture();

        // 2. 构造entity到数据库中更新
        Picture updateObj = new Picture();
        updateObj.setId(pic.getId());//注意:这里要额外设置一下! 因为更新也是要根据这个里面的id来去查找然后更新的呀
        String name = updatePictureRequest.getName();
        String intro = updatePictureRequest.getIntroduction();
        String category = updatePictureRequest.getCategory();
        List<String> tags = updatePictureRequest.getTags();

        if (name != null) updateObj.setName(name);
        if (intro != null) updateObj.setIntroduction(intro);
        if (category != null) updateObj.setCategory(category);
        if (tags != null) updateObj.setTags(JSONUtil.toJsonStr(tags));

        //图片更新后 需要将该图片信息更改为待审核
        fillReviewParams(updateObj, loginUser);
        updateObj.setEditTime(LocalDateTime.now());

        ThrowUtils.throwIf(!updateById(updateObj), ErrorCode.OPERATION_ERROR, "数据库中更新失败");
        return true;
    }

    /**
     * 分页查询 管理员专属 不脱敏
     *
     * @param queryPagePicture
     * @return
     */
    @Override
    public Page<Picture> queryPagePicture(QueryPagePicture queryPagePicture, HttpServletRequest httpServletRequest) {
        User loginUser = (User) httpServletRequest.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR, "先去登录");
//        判断是不是管理员权限
        ThrowUtils.throwIf(!isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR);
        //判断是不是查询的个人空间中的图片
        Long spaceId = queryPagePicture.getSpaceId();
        //如果查询的是公共图库的图片 那么就让查询查询出spaceId为null的所有图片
        if (spaceId == null) {
            queryPagePicture.setNullSpace(true);
        }

        //通过分页参数 条件获取pic数据
        return page(new Page<>(
                queryPagePicture.getCurrent(),
                queryPagePicture.getPageSize()
        ), getEq(queryPagePicture));
    }


    @Resource
    StringRedisTemplate stringRedisTemplate;

    /**
     * 查询图片集合 (不仅用户可以手动查询图片利用这个方法 前端自动展示首页分类也是调用该方法的(传入首页的标签参数还有分页参数即可呀))
     *
     * @param queryPagePicture
     * @param httpServletRequest
     * @return
     */
    @Override
    public Page<PictureVO> queryPictureVO(QueryPagePicture queryPagePicture, HttpServletRequest httpServletRequest) {
        //如果查询的图片数大于20 那么就直接设置成20
        if (queryPagePicture.getPageSize() > 20) {
            queryPagePicture.setPageSize(20);
        }

        // 新-判断是查询私人空间的照片 还是 公共图库
        Long spaceId = queryPagePicture.getSpaceId();
        //如果查询的是公共图库的照片 那么setNullSpace(true):查询spaceId为空的 PictureRoleEnum.PASS.getValue():查询审核通过的
        if (spaceId == null) {
            queryPagePicture.setNullSpace(true);
            queryPagePicture.setReviewStatus(PictureRoleEnum.PASS.getValue());
        } else {
            //如果是私有空间图库 那么就需要校验是不是该空间的主人
            Space space = spaceService.getById(spaceId);
            User loginUser = userService.getLoginUser(httpServletRequest);
            ThrowUtils.throwIf(!space.getUserId().equals(loginUser.getId()), ErrorCode.FORBIDDEN_ERROR);
        }


        //1. 获取未脱敏的Picture集合 转VO类型
        Page<Picture> picturePage = queryPagePicture(queryPagePicture, httpServletRequest);
//        创建pictureVo类型的Page集合
        Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
        //Picture->picVO 类型转换
        List<PictureVO> pictureVOList = picturePage.getRecords().stream().map(PictureVO::objToVo).toList();
        //2. 给每个图片vo赋值用户信息
        //获取每个上传该图片的用户id
        List<Long> userIdList = picturePage.getRecords().stream().map(Picture::getUserId).toList();
        //获取每一个用户的信息 通过这个用户id集合
        //问: 如何给每个图片赋值对应的用户 用最快的速度 反正绝对不是List 而是map
        //用LIst的弊端 太慢了: 如果usersList 那么外层循环是图片 拿一个Picture元素出来 是不是每次都需要遍历userList 通过userID来找对应的user信息? 还是通过userId来从数据库中找对应的用户信息? 都太慢了
        //答: 如果用:map<UserId,User信息>  那么就可以直接遍历picture集合 然后拿到一个picture元素之后 直接通过里面的Userid来找到对应的User信息 直接从map中get即可
        //将通过Userid将User数据分组 实现Userid<->User 所以就可以直接通过Userid来直接获取User数据 不要看这个是List<User> 但其实里面只有一条对应的数据而已
        if (!CollUtil.isEmpty(userIdList)) {//用户集合不为空的情况下 再去填充用户信息集合
            Map<Long, List<User>> useridUserMap = userService.listByIds(userIdList).stream()
                    .collect(Collectors.groupingBy(User::getId));
            //将每个PicVO元素中的UserVO都进行赋值
            for (PictureVO pictureVO : pictureVOList) {
                //思路:遍历每一个Picture 获取里面的UserId 然后通过map来获取对应的User信息 然后赋值进去即可
                Long userId = pictureVO.getUserId();
                if (useridUserMap.containsKey(userId)) {
//                useridUserMap.get(userId).get(0) 这就是通过userId来获取对应的User信息  map是这样的 Map<Long, List<User>> 这list里面的第一条数据就是对应的user数据
                    pictureVO.setUserVO(UserVO.objToVo(useridUserMap.get(userId).get(0)));
                }
            }
        }

        //3. 返回数据
        pictureVOPage.setRecords(pictureVOList);//将设置好的pic集合set到Records中
        return pictureVOPage;
    }

    /**
     * 修改图片的审查状态
     *
     * @param request
     * @param httpServletRequest
     */
    @Override
    public void doReviewPicture(ReviewRequest request, HttpServletRequest httpServletRequest) {
        //1. 校验数据
        //如果请求数据都不存在
        ThrowUtils.throwIf(request == null, ErrorCode.NOT_FOUND_ERROR);
        //判断是否登录并且是管理员
        isLoginAdmin(httpServletRequest);
        Long picId = request.getId();
        ThrowUtils.throwIf(request.getReviewStatus() == null, ErrorCode.PARAMS_ERROR, "审核状态不能为空");
        ThrowUtils.throwIf(picId == null, ErrorCode.PARAMS_ERROR, "id为空");
        //获取要修改的状态 如果要判断待审核的图片的状态是待审核?? 那么就抛错
        PictureRoleEnum enumByValue = PictureRoleEnum.getEnumByValue(request.getReviewStatus());
        ThrowUtils.throwIf(enumByValue == null, ErrorCode.PARAMS_ERROR, "无效的审核状态");
        if (PictureRoleEnum.REVIEWING.equals(enumByValue)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "对待审查的图片怎么更改状态为带审查???");
        }
        Picture picture = getById(picId);
        ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR, "数据库中根据id未找到该图片信息");
        //判断请求数据中要修改的状态 是否和 图片原有的状态一致 那么就跑错
        if (picture.getReviewStatus().equals(request.getReviewStatus())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "该图片已被审核");
        }
        //获取当前审核人的id
        Long adminId = getLoginUser(httpServletRequest).getId();
//        赋值状态 然后更新 这里为什么要new一个新的 不用byId查出来的Picture呢? 因为我只需要update这四个 而不需要update所有的状态
        Picture updatePicture = new Picture();
        updatePicture.setId(picId);//set一下id
        updatePicture.setReviewerId(adminId);
        updatePicture.setReviewTime(LocalDateTime.now());
        updatePicture.setReviewStatus(request.getReviewStatus());
        updatePicture.setReviewMessage(request.getReviewMessage());
        ThrowUtils.throwIf(!updateById(updatePicture), ErrorCode.OPERATION_ERROR, "更新失败");
    }


    @Resource
    UnsplashConfig unsplashConfig;

    /**
     * 根据关键词搜索图片(有质量的图库)
     *
     * @param count
     * @param query
     * @return
     */
    private List<String> searchPicture(Long count, String query) {
        ThrowUtils.throwIf(StrUtil.isBlank(query), ErrorCode.NOT_FOUND_ERROR, "搜索词呢???");
        String API = unsplashConfig.getApi();
        // 确保请求数量在合法范围内（Unsplash API 要求 per_page 最大为 30）
        count = 10L;//查询10条


        // 对中文关键词进行 URL 编码（例如 "猫" → "%E7%8C%AB"），避免请求出错
        String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);

        // 构造完整的 API 请求地址
        // 示例：https://api.unsplash.com/search/photos?query=%E7%8C%AB&per_page=5&client_id=xxx
        String url = String.format(
                "https://api.unsplash.com/search/photos?query=%s&per_page=%d&client_id=%s",
                encodedQuery, count, API
        );

        // 创建一个 HTTP 客户端（Java 11+ 内置）
        HttpClient client = HttpClient.newHttpClient();

        // 构建 HTTP GET 请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))           // 设置请求地址
                .header("Accept-Version", "v1") // 指定使用 API v1 版本（官方推荐）
                .build();                       // 完成构建
        JSONArray results;
        try {
            // 发送请求并获取响应（以字符串形式接收 JSON 数据）
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

            // 检查 HTTP 响应状态码是否为 200（成功）
            if (response.statusCode() != 200) {
                System.err.println("请求失败: " + response.statusCode());
                System.err.println("响应内容: " + response.body()); // 打印错误详情（如 rate limit）
                throw new RuntimeException("API 请求失败，请检查 Access Key 或网络");
            }

            // 将返回的 JSON 字符串解析为 JSONObject 对象
            JSONObject json = new JSONObject(response.body());

            // 从 JSON 中提取 "results" 数组（包含所有搜索结果）
            results = json.getJSONArray("results");
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 创建一个列表，用于存储图片链接
        List<String> urls = new ArrayList<>();

        // 遍历每一张图片
        for (int i = 0; i < results.length(); i++) {
            // 获取当前图片对象
            JSONObject photo = results.getJSONObject(i);
            // 进入 "urls" 字段，取出 "regular" 尺寸的图片链接（推荐用于展示）
            String imageUrl = photo.getJSONObject("urls").getString("regular");
            // 添加到结果列表中
            urls.add(imageUrl);
        }


        return urls;

    }


    /**
     * 通过关键词搜索来上传图片(通过bing图库)
     *
     * @param pictureUploadByBatchRequest
     * @param loginUser
     * @return
     */
    @Override
    public Integer uploadPictureBySearch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) throws IOException, InterruptedException {
        ThrowUtils.throwIf(pictureUploadByBatchRequest.getSearchText() == null, ErrorCode.NOT_FOUND_ERROR, "请输入搜索词");
        ThrowUtils.throwIf(pictureUploadByBatchRequest.getSearchText() == null, ErrorCode.NOT_FOUND_ERROR, "搜索词为空!!!");
//        不是管理员就拒掉
        ThrowUtils.throwIf(!isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR);
        Long searchCount = pictureUploadByBatchRequest.getCount();
        String searchTerms = pictureUploadByBatchRequest.getSearchText();
        String prefixName = pictureUploadByBatchRequest.getNamePrefix();
//        如果指定名字为空 那么就默认名字为搜索的关键词
        if (StrUtil.isBlank(prefixName)) {
            prefixName = searchTerms;
        }
        //1. 校验参数,最多智能获取10个图片
        if (searchCount > 10) {
            searchCount = 10L;
        }

        List<String> imgUrlList = searchPicture(searchCount, searchTerms);
        int uploadCount = 0;//用于记录上传成功的记录数
//        遍历里面的每个图片元素
        for (String fileUrl : imgUrlList) {
            String uploadUrl = null;
            if (StrUtil.isBlank(fileUrl)) {
                log.error("该图片为空地址:{}", fileUrl);
                continue;
            }
            //如果该图片地址不为空 那么寻找就要"?"的位置 要截取0-?前面的网址部分 ?后面的就丢弃 因为?后面的可能有敏感字符 并且丢弃url中的?后面的部分也没有问题
            int sub = fileUrl.indexOf("?");
            if (sub > -1) {
                uploadUrl = fileUrl.substring(0, sub);
            }
            try {
                //上传网址 先构造请求参数
                UploadPictureRequest pictureRequest = new UploadPictureRequest();
                pictureRequest.setFileUrl(fileUrl);
                pictureRequest.setFileName(prefixName + (uploadCount + 1));
//            上传网址中的图片
                PictureVO pictureVO = this.uploadPicture(uploadUrl, pictureRequest, loginUser);
                log.info("图片上传成功 id:{}", pictureVO.getId());
                uploadCount++;
            } catch (Exception e) {
                log.error("图片上传失败", e);//上传失败就continue上去 重新上传 一共上传count个
                continue;
            }
            if (uploadCount == searchCount) {
                break;
            }
        }
        //返回实际上传的记录数
        return uploadCount;
    }


//
//    /**
//     * 通过关键词搜索来上传图片(通过bing图库)
//     *
//     * @param pictureUploadByBatchRequest
//     * @param loginUser
//     * @return
//     */
//    @Override
//    public Integer uploadPictureBySearch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) {
//        ThrowUtils.throwIf(pictureUploadByBatchRequest.getSearchText() == null, ErrorCode.NOT_FOUND_ERROR, "搜索词为空!!!");
////        不是管理员就拒掉
//        ThrowUtils.throwIf(!isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR);
//        Long searchCount = pictureUploadByBatchRequest.getCount();
//        String searchTerms = pictureUploadByBatchRequest.getSearchText();
//        String prefixName = pictureUploadByBatchRequest.getNamePrefix();
////        如果指定名字为空 那么就默认名字为搜索的关键词
//        if (StrUtil.isBlank(prefixName)) {
//            prefixName = searchTerms;
//        }
//        //1. 校验参数,最多智能获取10个图片
//        if (searchCount > 10) {
//            searchCount = 10L;
//        }
//        //2. 构造请求的URL并获取图片内容    https://cn.bing.com/images/async?q=%s&mmasync=1中的%s就是要搜索的内容 这里利用请求参数searchTerms进行填充
//        String fetchURL = String.format("https://unsplash.com/s/photos/%s", searchTerms);
//        Document document;
//        try {
//            //用jsoup发起httpGet请求 获取网页的HTML内容
//            document = Jsoup.connect(fetchURL).get();
//        } catch (IOException e) {
//            log.error("图片获取失败 地址为:{}", fetchURL);
//            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
//        }
//
//        //3. 获取dgControl元素(这个是包含数据的div容器)
//        Element div = document.getElementsByClass("dgControl").first();
//        if (ObjUtil.isEmpty(div)) {
//            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "获取元素失败 搜的什么玩意");
//        }
//
//        //4. 获取div里面的照片 然后遍历上传图片
//        Elements imgElementList = div.select("img.mimg");
//        int uploadCount = 0;//用于记录上传成功的记录数
////        遍历里面的每个图片元素
//        for (Element element : imgElementList) {
//            //获取该图片的url
//            String fileUrl = element.attr("src");
//            String uploadUrl = null;
//            if (StrUtil.isBlank(fileUrl)) {
//                log.error("该图片为空地址:{}", fileUrl);
//                continue;
//            }
//            //如果该图片地址不为空 那么寻找就要"?"的位置 要截取0-?前面的网址部分 ?后面的就丢弃 因为?后面的可能有敏感字符 并且丢弃url中的?后面的部分也没有问题
//            int sub = fileUrl.indexOf("?");
//            if (sub > -1) {
//                uploadUrl = fileUrl.substring(0, sub);
//            }
//            try {
//                //上传网址 先构造请求参数
//                UploadPictureRequest pictureRequest = new UploadPictureRequest();
//                pictureRequest.setFileUrl(fileUrl);
//                pictureRequest.setFileName(prefixName + (uploadCount + 1));
    /// /            上传网址中的图片
//                PictureVO pictureVO = this.uploadPicture(uploadUrl, pictureRequest, loginUser);
//                log.info("图片上传成功 id:{}", pictureVO.getId());
//                uploadCount++;
//            } catch (Exception e) {
//                log.error("图片上传失败", e);//上传失败就continue上去 重新上传 一共上传count个
//                continue;
//            }
//            if (uploadCount == searchCount) {
//                break;
//            }
//        }
//        //返回实际上传的记录数
//        return uploadCount;
//    }


    //本地缓存
    private final Cache<String, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(1024) //容量设置成可以存储1024个缓存项
            .maximumSize(10_000L) //最大存储10000条数据 超过这个值的时候就用淘汰策略
            //缓存5分钟之后移除 数据写入后5分钟自动过期 过期后就是null了(不过读取为null的时候就从mysql中读取出来然后从重新赋值)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build();

    @Resource
    RedissonClient redissonClient;

    /**
     * 获取首页数据(高查询需求) 这里利用Redis优化 并且解决雪崩和击穿问题
     *
     * @param queryPagePicture
     * @param httpServletRequest
     * @return
     */
    @Override
    public Page<PictureVO> queryHomePagePicture(QueryPagePicture queryPagePicture, HttpServletRequest httpServletRequest) {
        //1. 先利用传过来的查询条件生成key 然后看看本地是否有数据
        //生成key:先将该条件们json化 然后用md5hash成一个key(节省空间)
        String queryJsonStr = JSONUtil.toJsonStr(queryPagePicture);
        //md5生成key
        String queryMd5 = DigestUtils.md5DigestAsHex(queryJsonStr.getBytes());
        //拼接成一个完整的key:
        String caffeineKey = CaCheConstant.CAFFEINE_KEY_PRE + queryMd5;
        //如果本地有数据那么直接返回
        String voJson = LOCAL_CACHE.getIfPresent(caffeineKey);
        if (voJson != null) {
            return JSONUtil.toBean(voJson, Page.class);
        }
        //2. 否则本地缓存中没有数据那么就从Redis中获取数据 并且将数据存放到Caffeine本地缓存中
        String redisKey = CaCheConstant.REDIS_KEY_PRE + queryMd5;
        //从Redis中获取数据
        voJson = stringRedisTemplate.opsForValue().get(redisKey);
        // 2. 如果redis中对应的该条件不为空 那么就直接将数据返回
        if (voJson != null) {
            //将Redis中获取的数据也保存到本地缓存Caffeine中 并且将Redis中查找出来的数据返回回去
            LOCAL_CACHE.put(caffeineKey, voJson);
            Page<PictureVO> pictureVOPage = JSONUtil.toBean(voJson, Page.class);
            return pictureVOPage;
        }

        //3. 如果Redis,Caffeine中都没有数据 那么就在Mysql中将数据查找出来 然后再将数据放到这两个缓存中
        //缓存击穿问题: 也叫热点key问题 就是一个被高并发访问的key突然失效了 无数的请求访问会在瞬间给数据库带来压力(互斥锁)
        //解决缓存击穿:互斥锁 发现key从Redis中找不到数据了 那么就通过该key去获取锁 获取锁成功->执行mysql查询Redis存入操作 获取锁失败->睡眠重试 这样就避免了多个线程同时访问数据库 造成Mysql压力山大
        //4. 缓存击穿问题解决:
        String lockKey = redisKey + CaCheConstant.LOCK_KEY;//锁key
        //4.1 拿到锁 利用redisson获取锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
            //让当前线程获取锁 最大等待时间是3秒(获取锁->获取不到->等待3秒) 最多持有锁的时间是30秒
            boolean isLock = lock.tryLock(3, 30, TimeUnit.SECONDS);
            //4.2 如果获取到了锁->看看Redis中有没有数据 有的话直接返回(双重检查) 如果Redis没有数据->执行Mysql查询,缓存存入任务
            if (isLock) {
                try {
                    return getRedisVoOrMysql(queryPagePicture, httpServletRequest, redisKey, caffeineKey);
                } finally {
                    //释放锁
                    lock.unlock();
                }
            } else {
                //4.3 如果没有获取到锁 那么先睡眠(说不定睡会之后其他线程任务就执行完了Redis中就有数据了) 然后尝试从Redis中获取数据 尝试3次 获取不到就用兜底策略 直接查询Mysql返回结果
                //避退重试策略 重试从Redis中获取三次
                int retryCount = 0;
                while (retryCount < 3) {
                    Thread.sleep(50L * (retryCount + 1));
                    voJson = stringRedisTemplate.opsForValue().get(redisKey);
                    if (voJson != null) {
                        return JSONUtil.toBean(voJson, Page.class);
                    }
                    retryCount++;
                }
                //最后从数据库中返回 不缓存结果
                return queryPictureVO(queryPagePicture, httpServletRequest);
            }
        } catch (InterruptedException e) {
            // 中断当前线程 然后直接查询数据库后返回
            Thread.currentThread().interrupt();
            return queryPictureVO(queryPagePicture, httpServletRequest); // 直接查询数据库
        }
    }


    /**
     * 首页搜索框搜索图片
     *
     * @param pictureUploadByBatchRequest
     * @param loginUser
     * @return
     */
    @Override
    public Page<PictureVO> uploadPictureBySearchHomePage(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) {
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR, "去登录再来搜索");
        String searchText = pictureUploadByBatchRequest.getSearchText();
        if (searchText == null) {
            searchText = "猫咪";
        }
        long count = 10L;
        List<String> strings = searchPicture(count, searchText);
        Page<PictureVO> pictureVOPage = new Page<>(1, count);
        List<PictureVO> pictureList = new ArrayList<>();
        for (String url : strings) {
            PictureVO pictureVO = new PictureVO();
            pictureVO.setUrl(url);
            pictureVO.setThumbnailUrl(url);
            pictureVO.setName(searchText + UUID.randomUUID());
            pictureList.add(pictureVO);
        }
        pictureVOPage.setRecords(pictureList);
        return pictureVOPage;
    }

    /**
     * 看看Redis中有没有数据 如果有 那么直接拿Redis中的数据 如果没有那么就执行Mysql查询,缓存存入任务
     *
     * @param queryPagePicture
     * @param httpServletRequest
     * @param redisKey
     * @param caffeineKey
     * @return
     */
    private Page<PictureVO> getRedisVoOrMysql(QueryPagePicture queryPagePicture, HttpServletRequest httpServletRequest, String redisKey, String caffeineKey) {
        String voJson;
        //然后尝试从Redis中获取数据
        voJson = stringRedisTemplate.opsForValue().get(redisKey);
        if (voJson != null) {
            return JSONUtil.toBean(voJson, Page.class);
        }
        //Redis中没有->执行Mysql查询,缓存存入任务
        return getVOPutCaChe(queryPagePicture, httpServletRequest, caffeineKey, redisKey);
    }


    /**
     * 执行查询Mysql,Redis存入操作
     *
     * @param queryPagePicture
     * @param httpServletRequest
     * @param caffeineKey
     * @param redisKey
     * @return
     */
    private Page<PictureVO> getVOPutCaChe(QueryPagePicture queryPagePicture, HttpServletRequest httpServletRequest, String caffeineKey, String redisKey) {
        String voJson;
        //如果抢到了锁 那么就去执行mysql查询Redis存入操作
        // 3. 如果为空 那么就从mysql中获取数据 然后再装入到Redis中
        Page<PictureVO> pictureVOPage = queryPictureVO(queryPagePicture, httpServletRequest);

        //雪崩优化
        //雪崩优化1: 区分空结果和非空结果 如果查询出来的结果是否为空 如果为空 那么就要存储空值即可 并且设置时间只需要设置1-2分钟即可
        boolean isEmpty = CollectionUtils.isEmpty(pictureVOPage.getRecords());
        int expiredTime;
        //雪崩优化2: 判断是结果否有数据
        if (!isEmpty) {
            //对于非空结果 设置时间为5-10分钟过期
            expiredTime = 300 + RandomUtil.randomInt(0, 300);//在300秒的基础上再随机加上0到300秒就可以实现5-10分钟
        } else {
            //对于查询出来的空结果就是1-2分钟过期
            expiredTime = 60 + RandomUtil.randomInt(0, 60);
        }
        //序列化后再存入json中
        voJson = JSONUtil.toJsonStr(pictureVOPage);
        // 4. 将数据序列化后装入Redis并且处理缓存雪崩,击穿问题
        //设置过期时间 为了防止缓存雪崩:如果在同一时间内一个key失效 那么所有请求都会打到数据库上 导致压力大 数据库就直接挂了 所以就设置不要在同一个时间过期 设置一个5-10分钟的随机时间
        LOCAL_CACHE.put(caffeineKey, voJson);
        //防雪崩策略: 就算这里mysql查询出来的是空值 也会存储到redis中 再次查询这个条件就直接从redis中查询空值返回 请求就不是打到数据库上了
        stringRedisTemplate.opsForValue().set(redisKey, voJson, expiredTime, TimeUnit.SECONDS);

        //  优化3：记录缓存命中率 方便后续记录 increment:当使用了一次缓存 那么这个key对应的count就++
        stringRedisTemplate.opsForValue().increment("cache:homepage:count");
        if (isEmpty) {
            //这里也记录一下访问空值的次数情况 如果多的话就说明有人想压垮我的数据库
            stringRedisTemplate.opsForValue().increment("cache:homepage:empty_count");
        }

        return pictureVOPage;
    }


    /**
     * 获取锁
     *
     * @param key 独有的锁key 返回值就是钥匙 如果上锁成功 那么就返回这个钥匙 如果上锁失败就返回null 这个钥匙可以是uuid也可以是当前线程的id
     * @return
     */
    private String tryLock(String key) {
        //生成"钥匙" 当前线程的唯一标识 去解锁的时候需要通过key来获取看看这个lockValue是否一致 一致才给删除
        String lockValue = UUID.randomUUID().toString();
        //利用该key来创建一个1 如果通过该key创建成功->无锁->执行Mysql查询Redis存入任务 如果创建失败->有锁->睡眠
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, lockValue, CaCheConstant.LOCK_TIME_OUT, TimeUnit.SECONDS);
        //上锁成功就返回钥匙 上锁失败就返回null
        if (Boolean.TRUE.equals(isLock)) {
            return lockValue;
        }
        return null;
    }

    /**
     * 释放锁 就直接将该key删除即可
     *
     * @param key
     */
    private void unLock(String key, String lockValue) {
        if (key == null && lockValue == null) {
            return;
        }
        //通过key来获取上锁时的lockValue 看看和当前线程手上的lockValue是否一致 如果一致才说明这个锁是当前线程上的锁 这才给解锁 否则不让删除
//        String script = "if redis.call('get',keys[1])==argv[1] then return redis.call('del',keys[1]) else return 0 end";
        //执行这个lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //读取脚本代码
        redisScript.setScriptText(String.valueOf(new ClassPathResource("scripts/unlock.lua")));
        redisScript.setResultType(Long.class);//返回值是一个整形
        //Collections.singletonList(key)将该key转换成一个list再传进去 key列表接收的是一个集合 然后value列表接收的是一个可变参数
        stringRedisTemplate.execute(redisScript, Collections.singletonList(key), lockValue);
    }


    /**
     * 判断是否登录 是不是管理员 如果不是管理员/没有登录就抛异常
     */
    private void isLoginAdmin(HttpServletRequest httpServletRequest) {
        //获取当前登录的用户
        User user = getLoginUser(httpServletRequest);
        //判断该用户是不是管理员 不是管理员直接拦截
        ThrowUtils.throwIf(!isAdmin(user), ErrorCode.NO_AUTH_ERROR);
    }

    private User getLoginUser(HttpServletRequest httpServletRequest) {
        return (User) httpServletRequest.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
    }


    /**
     * 获取查询条件
     *
     * @param queryPagePicture
     * @return
     */
    private LambdaQueryWrapper<Picture> getEq(QueryPagePicture queryPagePicture) {
        if (queryPagePicture == null) {
            return new LambdaQueryWrapper<>();
        }

        LambdaQueryWrapper<Picture> wrapper = new LambdaQueryWrapper<>();

        // ID 精确匹配（通常用于详情查询，但保留）
        if (queryPagePicture.getId() != null) {
            wrapper.eq(Picture::getId, queryPagePicture.getId());
        }

        // 字符串字段：非空才查询（避免查 "" 或 null）
        if (StrUtil.isNotBlank(queryPagePicture.getName())) {
            wrapper.like(Picture::getName, queryPagePicture.getName());
        }
        if (StrUtil.isNotBlank(queryPagePicture.getIntroduction())) {
            wrapper.like(Picture::getIntroduction, queryPagePicture.getIntroduction());
        }
        if (StrUtil.isNotBlank(queryPagePicture.getCategory())) {
            wrapper.like(Picture::getCategory, queryPagePicture.getCategory());
        }

        // 数值字段：非 null 才查询
        if (queryPagePicture.getPicSize() != null) {
            wrapper.eq(Picture::getPicSize, queryPagePicture.getPicSize());
        }
        if (queryPagePicture.getUserId() != null) {
            wrapper.eq(Picture::getUserId, queryPagePicture.getUserId());
        }

        //如果spaceId不为空 那么就查询出对应空间中的照片出来
        if (queryPagePicture.getSpaceId() != null) {
            wrapper.eq(Picture::getSpaceId, queryPagePicture.getSpaceId());
        }

        //>=stratTime
        wrapper.ge(ObjUtil.isNotNull(queryPagePicture.getStratTime()), Picture::getEditTime, queryPagePicture.getStratTime());
        //<endTime
        wrapper.lt(ObjUtil.isNotNull(queryPagePicture.getStratTime()), Picture::getEditTime, queryPagePicture.getStratTime());

        //queryPagePicture.getNullSpace()为true:查询共有图库 SpaceId为null的
        //queryPagePicture.getNullSpace()为false SpaceId中有值 查询私人空间图片 那么这个就跳过 并且SpaceId已经有值有条件
        wrapper.isNull(queryPagePicture.getNullSpace(), Picture::getSpaceId);

        // 字符串格式
        if (StrUtil.isNotBlank(queryPagePicture.getPicFormat())) {
            wrapper.eq(Picture::getPicFormat, queryPagePicture.getPicFormat());
        }

        // 时间字段
        if (queryPagePicture.getCreateTime() != null) {
            wrapper.eq(Picture::getCreateTime, queryPagePicture.getCreateTime());
        }

        // 审核状态 如果是查询VO的 也就是普通用户查询 那么这个条件就会强行设置成pass 已通过的审核状态
        if (queryPagePicture.getReviewStatus() != null) {
            wrapper.eq(Picture::getReviewStatus, queryPagePicture.getReviewStatus());
        }


//        标签集合 为什么要用List集合? 如果是String:["风景","治愈"] 咋查啊 所以要用List集合遍历一个一个遍历出来 一个标签一个条件
        List<String> tags = queryPagePicture.getTags();
//        如果集合不为空
        if (CollUtil.isNotEmpty(tags)) {
//            遍历该集合 设置条件
            for (String tag : tags) {
                wrapper.like(Picture::getTags, "\"" + tag + "\""); //栗子:list有:[风景,治愈] 这些标签 那么就需要根据"风景"和"治愈" 在图片的标签中查询 数据库中的图片标签是JSONString 是这样的:["风景","治愈"]
            }
        }

        return wrapper;
    }

    /**
     * 从cos中删除该图片
     *
     * @param picture
     */
    @Async//异步执行 数据库中的记录删除了就直接返回成功 从cos中删除图片就新建一个线程来处理
    public void cosDeletePicture(Picture picture) {
        ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);
        String url = picture.getUrl();
        Long count = lambdaQuery().eq(Picture::getUrl, url).count();
        if (count > 1) {
            log.info("该图片的url关联多条记录 先不删除{}", url);
            return;
        }
        try {
            //从cos中将该图片给删除
            cosManager.deleteObject(picture.getUrl());
            //删除缩略图
            cosManager.deleteObject(picture.getThumbnailUrl());

        } catch (Exception e) {//抛出该异常就可以实现事务回滚
            // 如果cos删除失败 那么就回滚 就算是先操作数据库 后来这里删除cos中的图片如果cos删除失败 就来这里抛出异常(必须是RunTimeException等类型的异常) 那么就会触发事务回滚 先前的数据库操作也会回滚
            log.error("COS 删除失败，残留在cos中 需要手动删除。picId: {}, url: {}", picture.getId(), picture.getUrl(), e);
            //只记录不抛出异常 数据库中删除就可以 cos中删没删除问题不大
            //比如 我更新图片 新图片已经上传 数据库中的url已经更新成新地址 但是删除老图片的时候抛出了异常 触发事务回滚 那么数据库中又回到了老图片的地址 但是新图片都上传了 所以说白了 cos中删除图片问题不大
        }
    }


//    /**
//     * 通过图片的url来提取里面的key(桶中对应的唯一地址) 其实不需要这么麻烦 直接通过url也可以直接删除
//     *
//     * @param url
//     * @return
//     */
//    private String getCosKey(String url) {
//        ThrowUtils.throwIf(StrUtil.isBlank(url), ErrorCode.NOT_FOUND_ERROR, "url为空:" + url);
////        url固定的前缀(桶名字+域地址)
////        String prefix = "https://mumu-1385850220.cos.ap-nanjing.myqcloud.com/";
//        String prefix = cosClientConfig.getHost() + "/";//这里肯定要用动态的host 不要写死了
////        存储结果
//        String result;
////        判断该url前缀是不是mumu-1385850220.cos.ap-nanjing.myqcloud.com/ 如果是的 那么就从prefix.length()开始截取该url(跳过)
//        if (url.startsWith(prefix)) {
//            result = url.substring(prefix.length());
//        } else {
////         如果该rul前缀不是mumu-1385850220.cos.ap-nanjing.myqcloud.com/ 那么就保留原样子
////            result = url; // 如果前缀不存在，保留原字符串（或根据需求抛异常）
//            throw new BusinessException(ErrorCode.PARAMS_ERROR, "该图片的url不在我的桶中 无法删除 地址是:" + url);
//        }
//        return result;
//        // 输出: public/1987458163939049473/2025-11-09_5474588829585151.2025-10-30 181139.png
//    }


    /**
     * 判断当前用户是否是管理员
     */
    @Resource
    UserService userService;

    public Boolean isAdmin(User user) {
        return user != null && Objects.equals(user.getUserRole(), UserRoleEnum.ADMIN.getText());
    }


}
